{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center> Определение тональности мелодии"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Цель работы, или зачем оно все нам нужно"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тональность песни, ипользуется в качестве признака при построении рекомендательной системы для существующих аудиотреков и автоматического аннонтировании новых, где нужно большое количество признаков, построенных на мелодиии. А также как метафича для других алгоритмов машинного обучения.\n",
    "А ещё: <img src=\"../../img/why_not.jpeg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ### Кратко о требуемой музыкальной теории "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Определение:** Тональность – это закрепление положения музыкального лада за определёнными по высоте звучания музыкальными тонами, привязка к конкретному участку музыкального звукоряда. \n",
    "\n",
    "Сложно, да? Разберемся подробнее.\n",
    "\n",
    "Любая тональность состоит из двух состовляющих: тоники и лада\n",
    "<font color=\"blue\"><center>**тональность = тоника + лад**</font>\n",
    "    \n",
    "Лад – это система звуков, объединенных устойчивым центром – тоникой.\n",
    "Лады состоят из семи звуков. Ступени (ноты) лада принято обозначать римскими цифрами.\n",
    "Первая ступень, тоника – самый устойчивый звук лада.\n",
    "\n",
    "Два основных семиступенных лада – мажорный (мажор) и минорный (минор).  По характеру мажор сравнивают со светом, минор – с тенью. «Мажор» в переводе с итальянского языка означает больший, «минор» – меньший. \n",
    "\n",
    "**Мажор** – это такой лад, в котором на тонике образуется мажорное трезвучие. Оно называется тоническое трезвучие. \n",
    "\n",
    "**Минор** – это такой лад, в котором на котором образуется минорное трезвучие. Оно также называется тоническим.\n",
    "\n",
    "Тоническое трезвучие состоит из: I – III – V ступеней. Расстояние между соседними звуками в мажорном и минорном трезвучие разное. Мажор: 2 тона – 1,5 тона; минор:  1,5 тона – 2 тона.\n",
    "\n",
    "Подведем итог: если говорить грубо, то тоника, это ступень(или нота), вокруг которой строится вся мелодия, то есть центральная. А лад, в свою очередь, определяет настроение музыкальной композиции.\n",
    "\n",
    "Итак у нас есть 7 нот:\n",
    "- До (C);\n",
    "- Ре (D);\n",
    "- Ми (E);\n",
    "- Фа (F);\n",
    "- Соль (G);\n",
    "- Ля (A);\n",
    "- Си (B);\n",
    "\n",
    "которые могут быть тоникой, а так же 5 нот увеличенные на полтона (C#, D#, F#, G#, A#). И два вида ладов: мажор (major) и минор (minor). \n",
    "А значит тональностей у нас 24.\n",
    "<center>\n",
    "<img src=\"../../img/notes.jpg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Постановка задачи"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Исходя из всего выше сказанного, вопрос определения тональности сводтся к задаче классиффикации на 24 класса.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Данные"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В проекте мы используем 3 датасета:\n",
    "- **[GTZAN](http://visal.cs.cityu.edu.hk/downloads/gtzan-keys/)** \n",
    "Датасет стоит из 1000 музыкальных композиций по 30 секунд, данные размечены и собраны в файл desription_GTZAN.csv\n",
    "Разделим его на три части: 62,5% обучающая выборка + 12,5% валидационная GT_TV, 25% тестовая выборка GT_TE.\n",
    "- **[GiantSteps Key Dataset](https://github.com/GiantSteps/giantsteps-key-dataset)**\n",
    "Датасет стоит из 603 музыкальных композиций по 2 минуты, данные размечены и собраны в файл desription_GS.csv\n",
    "Эти данные будем использовать только как тестовые.\n",
    "- **[GiantSteps MTG Key Dataset](https://github.com/GiantSteps/giantsteps-mtg-key-dataset)**\n",
    "Датасет стоит из 1486 музыкальных композиций по 2 минуты, данные размечены и собраны в файл desription_GS_MTG.csv\n",
    "Этот датасет исспользуем как тренировойчной и валидационный "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Во всех трех датасетах изначально песни представленны в формате '.mp3'. Конвертируем эти файлы в формат '.wav', используя баш-скрипт convert_dl.sh\n",
    "После того как провели все манипуляции собираем все данные в [data](https://yadi.sk/d/piIGdyig3Uf6eQ)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     }
    },
    "colab_type": "code",
    "id": "024eRixSuLWy"
   },
   "outputs": [],
   "source": [
    "from __future__ import print_function\n",
    "import glob, os\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import pickle"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     }
    },
    "colab_type": "code",
    "id": "N8jY3BuCuLXE"
   },
   "outputs": [],
   "source": [
    "import seaborn as sns\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.style as ms\n",
    "ms.use('seaborn-muted')\n",
    "%matplotlib inline\n",
    "\n",
    "import IPython.display\n",
    "\n",
    "import madmom\n",
    "\n",
    "import librosa\n",
    "import librosa.display"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm import tqdm\n",
    "import wave\n",
    "from scipy.io import wavfile\n",
    "SAMPLE_RATE = 44100\n",
    "\n",
    "import seaborn as sns\n",
    "color = sns.color_palette()\n",
    "import plotly.offline as py\n",
    "py.init_notebook_mode(connected=True)\n",
    "import plotly.graph_objs as go\n",
    "import plotly.offline as offline\n",
    "offline.init_notebook_mode()\n",
    "import plotly.tools as tls\n",
    "\n",
    "# Math\n",
    "from scipy.fftpack import fft\n",
    "from scipy import signal\n",
    "from scipy.io import wavfile\n",
    "\n",
    "from sklearn.model_selection import train_test_split"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### GiantSteps Key Dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим, что у нас внутри датасетов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PATH = '../data/giantsteps-key-dataset-master/'\n",
    "description_GS = pd.read_csv(PATH + 'description_master.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GS.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы можем заметить, ничего лишнего. Только название файла и тональность.\n",
    "\n",
    "Теперь посмотрим как распределены данные по тональностям. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Total number of labels in training data : \",len(description_GS['Target'].value_counts()))\n",
    "print(\"Labels are : \", description_GS['Target'].unique())\n",
    "plt.figure(figsize=(15,8))\n",
    "audio_type = description_GS['Target'].value_counts()\n",
    "sns.barplot(audio_type.values, audio_type.index, palette=sns.color_palette(\"husl\", 24))\n",
    "for i, v in enumerate(audio_type.values):\n",
    "    plt.text(0.8,i,v,color='k',fontsize=12)\n",
    "plt.xticks(rotation='vertical')\n",
    "plt.xlabel('Frequency')\n",
    "plt.ylabel('Label Name')\n",
    "plt.title(\"Labels with their frequencies in training data\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## GiantSteps MTG Key Dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PATH = '../data/giantsteps-mtg-key-dataset-master/'\n",
    "description_GS_mtg = pd.read_csv(PATH + 'description_master.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GS_mtg.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Total number of labels in training data : \",len(description_GS_mtg['Target'].value_counts()))\n",
    "print(\"Labels are : \", description_GS_mtg['Target'].unique())\n",
    "plt.figure(figsize=(15,8))\n",
    "audio_type = description_GS_mtg['Target'].value_counts()\n",
    "sns.barplot(audio_type.values, audio_type.index, palette=sns.color_palette(\"husl\", 24))\n",
    "for i, v in enumerate(audio_type.values):\n",
    "    plt.text(0.8,i,v,color='k',fontsize=12)\n",
    "plt.xticks(rotation='vertical')\n",
    "plt.xlabel('Frequency')\n",
    "plt.ylabel('Label Name')\n",
    "plt.title(\"Labels with their frequencies in training data\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так-так. А тут у нас затесались нелегалы. Удаляем все записи у которых не определенна тональность."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GS_mtg.drop(index = description_GS_mtg[description_GS_mtg['Target'] =='None'].index,\n",
    "                                             axis=0, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Total number of labels in training data : \",len(description_GS_mtg['Target'].value_counts()))\n",
    "print(\"Labels are : \", description_GS_mtg['Target'].unique())\n",
    "plt.figure(figsize=(15,8))\n",
    "audio_type = description_GS_mtg['Target'].value_counts()\n",
    "sns.barplot(audio_type.values, audio_type.index, palette=sns.color_palette(\"husl\", 24))\n",
    "for i, v in enumerate(audio_type.values):\n",
    "    plt.text(0.8,i,v,color='k',fontsize=12)\n",
    "plt.xticks(rotation='vertical')\n",
    "plt.xlabel('Frequency')\n",
    "plt.ylabel('Label Name')\n",
    "plt.title(\"Labels with their frequencies in training data\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Славненько. Поехали дельше"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "aALzBvVsuLXQ"
   },
   "source": [
    "### GTZAN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     }
    },
    "colab_type": "code",
    "id": "Jvo-0Xa1uLXU"
   },
   "outputs": [],
   "source": [
    "description_GTZAN = pd.read_csv(\"../data/GTZAN/description.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GTZAN.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Total number of labels in training data : \",len(description_GTZAN['Target'].value_counts()))\n",
    "print(\"Labels are : \", description_GTZAN['Target'].unique())\n",
    "plt.figure(figsize=(15,8))\n",
    "audio_type = description_GTZAN['Target'].value_counts()\n",
    "sns.barplot(audio_type.values, audio_type.index, palette=sns.color_palette(\"husl\", 24))\n",
    "for i, v in enumerate(audio_type.values):\n",
    "    plt.text(0.8,i,v,color='k',fontsize=12)\n",
    "plt.xticks(rotation='vertical')\n",
    "plt.xlabel('Frequency')\n",
    "plt.ylabel('Label Name')\n",
    "plt.title(\"Labels with their frequencies in training data\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GTZAN.drop(index = description_GTZAN[description_GTZAN['Target'] =='None'].index,\n",
    "                                             axis=0, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Total number of labels in training data : \",len(description_GTZAN['Target'].value_counts()))\n",
    "print(\"Labels are : \", description_GTZAN['Target'].unique())\n",
    "plt.figure(figsize=(15,8))\n",
    "audio_type = description_GTZAN['Target'].value_counts()\n",
    "sns.barplot(audio_type.values, audio_type.index, palette=sns.color_palette(\"husl\", 24))\n",
    "for i, v in enumerate(audio_type.values):\n",
    "    plt.text(0.8,i,v,color='k',fontsize=12)\n",
    "plt.xticks(rotation='vertical')\n",
    "plt.xlabel('Frequency')\n",
    "plt.ylabel('Label Name')\n",
    "plt.title(\"Labels with their frequencies in training data\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим что же мы можем сделать с музыкой.\n",
    "\n",
    "Возьмем файл 32 из датасета GS_mtg\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "path_file = '../data/giantsteps-mtg-key-dataset-master/audio/' + description_GS_mtg.Name[32] + '.wav'\n",
    "y, sr = librosa.load(path_file, duration=30)\n",
    "plt.figure(figsize=(10, 4))\n",
    "librosa.display.waveplot(y, sr=sr)\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим из графика (а так же знаем еще со школы) звук - волна. Только в таком виде что-либо делать с ней   проблематично.\n",
    "Посмотрим на частотно-временное представление волны, то есть спектрограмму.\n",
    "\n",
    "**Спектрограмма** — изображение, показывающее зависимость спектральной плотности мощности сигнала от времени.\n",
    "\n",
    "Спектрограмма обычно создаются одним из двух способов: аппроксимируется, как набор фильтров, полученных из серии полосовых фильтров, или рассчитывается по сигналу времени, используя оконное преобразование Фурье.\n",
    "\n",
    "Создание спектрограммы с помощью оконного преобразования Фурье обычно выполняется методами цифровой обработки. Производится цифровая выборка данных во временной области. Сигнал разбивается на части, которые, как правило, перекрываются, и затем производится преобразование Фурье, чтобы рассчитать величину частотного спектра для каждой части. Каждая часть соответствует вертикальной линии на изображении — значение амплитуды в зависимости от частоты в каждый момент времени. Спектры или временные графики располагаются рядом на изображении или трёхмерной диаграмме.\n",
    "\n",
    "Оконное преобразование Фурье — это разновидность преобразования Фурье, определяемая следующим образом:\n",
    "\n",
    "$$F(t,\\omega) = \\int\\limits_{-\\infty}^\\infty f(\\tau) W(\\tau-t) e^{-i\\omega \\tau}\\,d\\tau$$\n",
    "\n",
    "где $W(τ − t)$ — некоторая оконная функция. \n",
    "\n",
    "В случае дискретного преобразования оконная функция используется аналогично:\n",
    "\n",
    "$$F(m,\\omega) = \\sum_{n=-\\infty}^{\\infty} f[n]w[n-m]e^{-j \\omega n} $$\n",
    "\n",
    "На основе спектра строится функция цветности. Хромограмма - интересное и мощное представление для музыкального аудио, в котором весь спектр проецируется на 12 ячеек, представляющих 12 отдельных полутонов (или цветности) музыкальной октавы. Поскольку в музыке ноты, расположенные на на растоянии в октаву (т.е 6 тонов) друг от друга, воспринимаются как особенно похожие, зная, что распределение цветности даже без абсолютной частоты (т.е. исходной октавы) может дать полезную музыкальную информацию об аудио - и может даже показать воспринимаемое музыкальное сходство, которое не проявляется в исходных спектрах."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chroma = librosa.feature.chroma_stft(y, sr = sr, n_fft=5000)\n",
    "plt.figure(figsize=(13, 4))\n",
    "librosa.display.specshow(chroma, y_axis='chroma', x_axis='time')\n",
    "plt.colorbar()\n",
    "plt.title('Chromagram')\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из хромограммы уже просматриваются особенности изманения с течением времени нот.\n",
    "\n",
    "Исходя из определения, что тоника - то самый устойчивая нота и вокруг нее крутится вся мелодия. Можно посмотреть ноту у которой будет наибольшее значение средней по времени хромограммы. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chroma.mean(axis = 1).argmax()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "9 - это нота Ля(A). Посмотрим на реальное значение. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description_GS_mtg.Target[32]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Юху. Они совпали. Имеет смысл предположить, что вычесленной функции цветности будет достаточно что бы определить тональность музыкальной композиции. \n",
    "\n",
    "Расчитаем для всех наших данных хромограммы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "output_extras": [
      {}
     ]
    },
    "colab_type": "code",
    "id": "BYlFxPwMuLXk",
    "outputId": "2754794a-9f97-465e-e49b-a593d0d73798"
   },
   "outputs": [],
   "source": [
    "n1 = description_GTZAN.shape[0]\n",
    "n2 = 12 # количество различных нот\n",
    "n3 = 431 * 2# за 10 секунд делается 431 кодр, а мы берем первые 20 секунд\n",
    "\n",
    "GTZAN_chroma = np.empty((n1, n2, n3))\n",
    "i = 0\n",
    "pathForData = '../data/GTZAN/'\n",
    "\n",
    "for file in tqdm(description_GTZAN.Name):\n",
    "    audio_path = pathForData + 'all/' + file + '.wav'\n",
    "    y, sr = librosa.load(audio_path, duration=20)\n",
    "    chroma = librosa.feature.chroma_stft(y, sr = sr, n_fft=5000)\n",
    "    GTZAN_chroma[i] = chroma\n",
    "    i += 1\n",
    "with open(pathForData + 'GTZAN_chromagram.txt', 'wb') as f:\n",
    "    pickle.dump(GTZAN_chroma, f, 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     }
    },
    "colab_type": "code",
    "id": "Whb17KGJuLXu"
   },
   "outputs": [],
   "source": [
    "n1 = description_GS_mtg.shape[0]\n",
    "\n",
    "GS_mtg_chroma = np.empty((n1, n2, n3))\n",
    "i = 0\n",
    "pathForData = '../data/giantsteps-mtg-key-dataset-master/'\n",
    "\n",
    "for file in tqdm(description_GS_mtg.Name):\n",
    "    audio_path = pathForData + 'audio/' + file + '.wav'\n",
    "    y, sr = librosa.load(audio_path, duration=20)\n",
    "    chroma = librosa.feature.chroma_stft(y, sr = sr, n_fft=5000)\n",
    "    GS_mtg_chroma[i] = chroma\n",
    "    i += 1\n",
    "with open(pathForData + 'GS_mtg_chromagram.txt', 'wb') as f:\n",
    "    pickle.dump(GS_mtg_chroma, f, 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n1 = description_GS.shape[0]\n",
    "\n",
    "GS_chroma = np.empty((n1, n2, n3))\n",
    "i = 0\n",
    "pathForData = '../data/giantsteps-key-dataset-master/'\n",
    "\n",
    "\n",
    "for file in tqdm(description_GS.Name):\n",
    "    audio_path = pathForData + 'audio/' + file + '.wav'\n",
    "    y, sr = librosa.load(audio_path, duration=20)\n",
    "    chroma = librosa.feature.chroma_stft(y, sr = sr, n_fft=5000)\n",
    "    GS_chroma[i] = chroma\n",
    "    i += 1\n",
    "with open(pathForData + 'GS_chromagram.txt', 'wb') as f:\n",
    "    pickle.dump(GS_chroma, f, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Модель"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import SVG\n",
    "from keras.utils.vis_utils import model_to_dot\n",
    "from keras.models import Sequential\n",
    "from keras.layers import Dense, Dropout, Activation, Flatten, Reshape\n",
    "from keras.layers import Conv2D, MaxPooling2D, AveragePooling2D\n",
    "from keras.optimizers import SGD, Adagrad\n",
    "from __future__ import division\n",
    "import keras\n",
    "import re\n",
    "from sklearn.model_selection import train_test_split\n",
    "from keras.layers.advanced_activations import ELU, Softmax\n",
    "from keras import backend as K\n",
    "from keras.callbacks import EarlyStopping, TensorBoard    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K.set_image_data_format('channels_first')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ну что ж, настало время поговорить о модели, которая будет определять тональности.\n",
    "\n",
    "Мы будем использавать сверточную нейронную сеть. 2 слоя сверток по 24 шаблона размерностью (12, 43), почему так?\n",
    "Мы предпологаем, что каждая тональность имеет свой особенный шаблон, размер которого определяется как количество уникальных нот (по хромограмме которая подается на вход) на количество кадров в 1 секунду. При последовательном применении этих 2-х свертках наша нейроная сеть лучше определяет, усредняет эти шаблоны.\n",
    "\n",
    "Далее мы усредняем по времени наши данные. После чего, исспользуя полносвязный слой, делим сначала на 48 выходов, для того что бы не потерять слишком много информации. А потом собственно определяем класс к которому относится объект.\n",
    "\n",
    "На всех слоях, кроме последнего исспользуется функция активации 'ELU'. \n",
    "На последнем 'softmax'.\n",
    "\n",
    "Вкачестве регуляризации на полносвязных слоях будем исспользовать dropout. Коэффициент будм подбирать на валидационной выборке. \n",
    "\n",
    "В качестве loss-функции будем исспользовать котегориальную крос-энтропию. А в качестве метрики acuracy.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_model(coef_dropout):\n",
    "    nclasses = 24 \n",
    "    count_frame_one_sec = 43\n",
    "    height = 12\n",
    "    width = 431 * 2\n",
    "    \n",
    "    model = Sequential()\n",
    "\n",
    "    model.add(Conv2D(nclasses, (height, count_frame_one_sec), \n",
    "                     padding='same',\n",
    "                     input_shape=(1, height, width)))\n",
    "    model.add(ELU())\n",
    "    model.add(Conv2D(nclasses, (height, count_frame_one_sec), \n",
    "                     padding='same', \n",
    "                     input_shape=(1, height, width)))\n",
    "    model.add(ELU())\n",
    "    model.add(AveragePooling2D(pool_size = (1, width)))\n",
    "\n",
    "    model.add(Flatten())\n",
    "\n",
    "    model.add(Dense(48))\n",
    "    model.add(ELU())\n",
    "    model.add(Dropout(0.5))\n",
    "\n",
    "    model.add(Dense(nclasses))\n",
    "    model.add(Softmax())\n",
    "\n",
    "    model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])\n",
    "    \n",
    "    return model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = get_model(0.5)\n",
    "SVG(model_to_dot(model, show_layer_names=True, show_shapes=True).create(prog='dot', format='svg'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь поговорим о метрике и оценках результатов.\n",
    "\n",
    "В этом проекте используется метрика MIREX - это общепринятая метрика для данной задачи. В ней учитываюстя не только абсолютно верно классифицированные объекты, но и верные не по  всем критериям, а именно:\n",
    "\n",
    "- Квинта(f). Это такой тип ошибки, когда верно определен лад, но тоника предказаная находится на расстоянии квинты(3.5 тона)  до реальной(или наоборот).\n",
    "- Относительный мажор/минор(r): лад определен неверно и а) определен мажор и тоника, на 1.5 тона выше; б) определен минор, а тоника на 1.5 тона ниже реальной.\n",
    "- Праллельный мажор/минор(p): лад определён не верно, а тоники совпадают\n",
    "\n",
    "<center> **MIREX = right + 0.5f + 0.3r + 0.2p**</center>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "relative_key = {\n",
    "     'C major': 'A minor'\n",
    "    ,'G major': 'E minor'\n",
    "    ,'F major': 'D minor'\n",
    "    ,'D major': 'B minor'\n",
    "    ,'A major': 'E minor'\n",
    "    ,'E major': 'C# minor'\n",
    "    ,'B major': 'G# minor'\n",
    "    ,'F# major': 'D# minor'\n",
    "    ,'C# major': 'A# minor'\n",
    "    ,'G# major': 'F minor'\n",
    "    ,'D# major': 'C minor'\n",
    "    ,'A# major': 'G minor'\n",
    "    \n",
    "    #other same, but reverse\n",
    "    ,'A minor': 'C major'\n",
    "    ,'E minor': 'G major'\n",
    "    ,'D minor': 'F major'\n",
    "    ,'B minor': 'D major'\n",
    "    ,'E minor': 'A major'\n",
    "    ,'C# minor': 'E major'\n",
    "    ,'G# minor': 'B major'\n",
    "    ,'D# minor': 'F# major'\n",
    "    ,'A# minor': 'C# major'\n",
    "    ,'F minor': 'G# major'\n",
    "    ,'C minor': 'D# major'\n",
    "    ,'G minor': 'A# major'\n",
    "    \n",
    "}\n",
    "fifth_key = {\n",
    "    'C': 'G',\n",
    "    'C#': 'G#',\n",
    "    'D': 'A',\n",
    "    'D#': 'A#',\n",
    "    'E': 'B',\n",
    "    'F': 'C',\n",
    "    'F#': 'C#',\n",
    "    'G': 'D',\n",
    "    'G#': 'D',\n",
    "    'A': 'E',\n",
    "    'A#': 'F',\n",
    "    'B': 'F#'\n",
    "    \n",
    "}\n",
    "col = pd.get_dummies(description_GTZAN.Target).columns"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def SetMaxProbabilityToOne(array):\n",
    "    return map(lambda x: 1 if(x == max(array)) else 0, array)\n",
    "\n",
    "def GetIndexOneValue(array):\n",
    "    for i, x in enumerate(array):\n",
    "        if(x != 0):\n",
    "            return i\n",
    "        \n",
    "def GetPredictNameAndYName(pred, y):\n",
    "    pred = SetMaxProbabilityToOne(pred)\n",
    "    \n",
    "    indPred = GetIndexOneValue(pred)\n",
    "    namePred = col[indPred]\n",
    "    \n",
    "    indY = GetIndexOneValue(y)\n",
    "    nameY = col[indY]\n",
    "    \n",
    "    return namePred, nameY\n",
    "\n",
    "def GetMIREXScoreOnSample(namePred, nameY):\n",
    "    if(namePred == nameY):\n",
    "        return 1\n",
    "    \n",
    "    isHaveRelativeKey = False\n",
    "    try:\n",
    "        isHaveRelativeKey = ((relative_key[namePred] == nameY) | (relative_key[nameY] == namePred))\n",
    "    except:\n",
    "        pass\n",
    "    \n",
    "    if(isHaveRelativeKey):\n",
    "        return 0.3\n",
    "\n",
    "    namePred = re.findall(r'\\w+[#]*', namePred)\n",
    "    nameY = re.findall(r'\\w+[#]*', nameY)\n",
    "\n",
    "    if(namePred[1] == nameY[1]) & ((fifth_key[namePred[0]] == nameY[0]) | (fifth_key[nameY[0]] == namePred[0])):\n",
    "\n",
    "        return 0.5\n",
    "    \n",
    "    if(namePred[0] == nameY[0]):\n",
    "        return 0.2\n",
    "    \n",
    "    return 0\n",
    "\n",
    "def GetMIREXScore(model, X, y):\n",
    "    score = 0.0\n",
    "    correctPredict = 0\n",
    "    correctGroundTruthKey = 0\n",
    "    correctRelativeMajMinKey = 0\n",
    "    correctParallelMajMinKey = 0\n",
    "    other = 0\n",
    "    for i in range(X.shape[0]):\n",
    "        pred = model.predict(X[i].reshape(1, 1, X.shape[2], X.shape[3]))[0]\n",
    "        yToFunc = y[i]\n",
    "        namePred, nameY = GetPredictNameAndYName(pred, yToFunc)\n",
    "        \n",
    "        scoreOnCurrentSample = GetMIREXScoreOnSample(namePred, nameY)\n",
    "        score += scoreOnCurrentSample\n",
    "        \n",
    "        if(scoreOnCurrentSample == 1):\n",
    "            correctPredict += 1\n",
    "        elif(scoreOnCurrentSample == 0.5):\n",
    "            correctGroundTruthKey += 1\n",
    "        elif(scoreOnCurrentSample == 0.3):\n",
    "            correctRelativeMajMinKey += 1\n",
    "        elif(scoreOnCurrentSample == 0.2):\n",
    "            correctParallelMajMinKey += 1\n",
    "        elif(scoreOnCurrentSample == 0):\n",
    "            other += 1\n",
    "        \n",
    "    score /= X.shape[0]\n",
    "    correctPredict \n",
    "    correctGroundTruthKey \n",
    "    correctRelativeMajMinKey \n",
    "    correctParallelMajMinKey \n",
    "    return score, X.shape[0], correctPredict, correctGroundTruthKey\\\n",
    "            , correctRelativeMajMinKey, correctParallelMajMinKey, other"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Приведем данные к нужному виду"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "GS_chroma.shape = (GS_chroma.shape[0], 1, n2, n3)\n",
    "t_GS = pd.get_dummies(description_GS.Target)\n",
    "y_GS = t_GS.as_matrix()\n",
    "\n",
    "GS_mtg_chroma.shape = (GS_mtg_chroma.shape[0], 1, n2, n3)\n",
    "t_GS_mtg = pd.get_dummies(description_GS_mtg.Target)\n",
    "y_GS_mtg = t_GS_mtg.as_matrix()\n",
    "\n",
    "GTZAN_chroma.shape = (GTZAN_chroma.shape[0], 1, n2, n3)\n",
    "t_GTZAN = pd.get_dummies(description_GTZAN.Target)\n",
    "y_GTZAN = t_GTZAN.as_matrix()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_GS, X_valid, y_train_GS, y_valid = train_test_split(GS_mtg_chroma, y_GS_mtg, test_size = 0.17, shuffle = True)\n",
    "\n",
    "coef_dropout = np.linspace(0.75, 0.25, 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = {}\n",
    "for coef in coef_dropout:\n",
    "    X_train, X_test, y_train, y_test = train_test_split(X_valid, y_valid, test_size = 0.25, shuffle = True)\n",
    "    model = get_model(coef)\n",
    "    model.fit(X_train, y_train, batch_size=32, epochs=50, validation_split=0, \n",
    "              shuffle=True, verbose=0)\n",
    "    score = GetMIREXScore(model, X_test, y_test)\n",
    "    str_score = 'coef-' + str(coef)\n",
    "    res[str_score] = score[0]\n",
    "    K.clear_session()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Обучение"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = get_model(0.75)\n",
    "model.fit(X_train_GS, y_train_GS, batch_size=32, epochs=50, validation_split=0.1, \n",
    "          shuffle=True, verbose=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "score = GetMIREXScore(model, GS_chroma, y_GS)\n",
    "print(score)\n",
    "X_train, X_test, y_train, y_test = train_test_split(GTZAN_chroma, y_GTZAN, test_size = 0.25, shuffle = True)\n",
    "score = GetMIREXScore(model, X_test, y_test)\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K.clear_session()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model1 = get_model(0.75)\n",
    "model1.fit(X_train, y_train, batch_size=32, epochs=50, validation_split=0.1, \n",
    "          shuffle=True, verbose=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "score = GetMIREXScore(model1, X_test, y_test)\n",
    "print(score)\n",
    "score = GetMIREXScore(model1, GS_chroma, y_GS)\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K.clear_session()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Оценка полученых результатов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы обучали нашу модель на двух разных датасетах (GeantStep_mtg и GTAZAN). И тестировали тоже на разных данных.\n",
    "Что на выходе (оценка по mirex, количество объектов в тесте, количество верных определенных объектовб кол-во квинт, кол-во относительных, количество параллельных, др. ошибки)\n",
    "Обучение на GS_mtg:\n",
    "- тест GS: (0.6165837479270317, 603, 317, 77, 37, 26, 146)\n",
    "\n",
    "- тест GTZAN: (0.6382775119617228, 209, 116, 21, 11, 18, 43)\n",
    "\n",
    "Обучение на GTZAN:\n",
    "- тест GS: (0.5688225538971808, 603, 278, 89, 43, 38, 155)\n",
    "- тест GTZAN: (0.6507177033492826, 209, 116, 27, 9, 19, 38)\n",
    "\n",
    "Чисто визуально кажется, что реезультат не велик, но мы можем заметить, что при рандоме наш результат был бы 0.04.\n",
    "Во всех случаях определенно верно более половины объектов и менее 1/3 произвольных ошибок, что является хорошим показателем.\n",
    "\n",
    "Из [cтатьи](https://arxiv.org/abs/1706.02921) вышедшей в 9.06.2017 лучшие результаты: 0.74\n",
    "\n",
    "А значит нам есть куда еще расти."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### О дальнейших путях развития"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Первое и, возможно, самое важное для решения подобных задач: нужно больше данных;\n",
    "- А когда много данных можно реализовать более сложную архетектуру нейронной сети;\n",
    "- Так же можно подумать об внедрении в продакшн, так как задача остается актуальной и по сей день. \n",
    "Хотя [некоторые](http://www.jordipons.me/apps/music-audio-tagging-at-scale-demo/) уже представили работающий протатип."
   ]
  }
 ],
 "metadata": {
  "colab": {
   "default_view": {},
   "name": "_1_ConvertToChroma.ipynb",
   "provenance": [],
   "version": "0.3.2",
   "views": {}
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
